package com.ftqh.tutorial.security.oauth2.provider.client;

import com.ftqh.tutorial.security.oauth2.provider.client.enums.RefreshTokenValidity;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * \* Created with IntelliJ IDEA.
 * \* User: Administrator
 * \* Date: 2017/1/5
 * \* Time: 18:36
 * \* To change this template use File | Settings | File Templates.
 * \* Description:
 * \
 */
@Service
public class OAuth2ClientDetailsService implements ClientDetailsService, ClientRegistrationService {
    private static final Log logger = LogFactory.getLog(JdbcClientDetailsService.class);
    private static RandomValueStringGenerator defaultClientSecretGenerator = new RandomValueStringGenerator(32);

    /*@Autowired
    private BCryptPasswordEncoder passwordEncoder;*/
    private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();

    private JsonMapper mapper = createJsonMapper();

    private OAuth2ClientDetailsRepository clientDetailsRepository;

    OAuth2ClientDetailsService(OAuth2ClientDetailsRepository clientDetailsRepository){
        this.clientDetailsRepository = clientDetailsRepository;
    }

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        return clientDetailsRepository.findOneByClientId(clientId)
                .<ClientRegistrationException>orElseThrow(() -> new NoSuchClientException("No client with requested id: " + clientId));
    }

    @Override
    public void addClientDetails(ClientDetails clientDetails) throws ClientAlreadyExistsException {
        if (clientDetailsRepository.findOneByClientId(clientDetails.getClientId()).isPresent()) {
            throw new ClientAlreadyExistsException("Client already exists: " + clientDetails.getClientId());
        }
        OAuth2ClientDetails oAuth2ClientDetails = this.getOauth2ClientDetailsForCreate(clientDetails);
        clientDetailsRepository.save(oAuth2ClientDetails);
    }

    @Override
    public void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException {
        OAuth2ClientDetails oAuth2ClientDetails = clientDetailsRepository.findOneByClientId(clientDetails.getClientId())
                .orElseThrow(() -> new NoSuchClientException("No client found with id = " + clientDetails.getClientId()));
        String json = null;
        try {
            if(clientDetails.getAdditionalInformation()!=null) {
                json = mapper.write(clientDetails.getAdditionalInformation());
            }
        }
        catch (Exception e) {
            logger.warn("Could not serialize additional information: " + clientDetails, e);
        }
        oAuth2ClientDetails.setResourceIds(clientDetails.getResourceIds() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
                .getResourceIds()) : null);
        oAuth2ClientDetails.setScope(clientDetails.getScope() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
                .getScope()) : null);
        oAuth2ClientDetails.setAuthorizedGrantTypes(clientDetails.getAuthorizedGrantTypes() != null ? StringUtils
                .collectionToCommaDelimitedString(clientDetails.getAuthorizedGrantTypes()) : null);
        oAuth2ClientDetails.setRegisteredRedirectUris(clientDetails.getRegisteredRedirectUri() != null ? StringUtils
                .collectionToCommaDelimitedString(clientDetails.getRegisteredRedirectUri()) : null);
        oAuth2ClientDetails.setAuthorities(clientDetails.getAuthorities() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
                .getAuthorities()) : null);
        oAuth2ClientDetails.setAccessTokenValiditySeconds(clientDetails.getAccessTokenValiditySeconds()!=null?clientDetails.getAccessTokenValiditySeconds():60 * 60 * 2);
        oAuth2ClientDetails.setRefreshTokenValiditySeconds(clientDetails.getRefreshTokenValiditySeconds() != null ? RefreshTokenValidity.fromSeconds(clientDetails.getRefreshTokenValiditySeconds()):RefreshTokenValidity.SEVEN_DAY);
        oAuth2ClientDetails.setAdditionalInfos(json);
        oAuth2ClientDetails.setAutoApprove(getAutoApproveScopes(clientDetails));

        clientDetailsRepository.save(oAuth2ClientDetails);
    }

    @Override
    public void updateClientSecret(String clientId, String secret) throws NoSuchClientException {
        int count = clientDetailsRepository.updateClientSecret(clientId, secret);
        if (count != 1) {
            throw new NoSuchClientException("No client found with id = " + clientId);
        }
    }
    @Transactional
    @Override
    public void removeClientDetails(String clientId) throws NoSuchClientException {
        OAuth2ClientDetails oAuth2ClientDetails = clientDetailsRepository.findOneByClientId(clientId)
                .<ClientRegistrationException>orElseThrow(() -> new NoSuchClientException("No client with requested id: " + clientId));
        clientDetailsRepository.delete(oAuth2ClientDetails);
    }

    @Override
    public List<ClientDetails> listClientDetails() {
        return clientDetailsRepository.findAll().parallelStream().map(transformBaseClientDetails).collect(Collectors.toList());
    }

    /*****下面的Public方法是为在OAuth2ClientDetails添加了自定义属性而使用的******/

    public Optional<OAuth2ClientDetails> findOne(String clientId) throws ClientRegistrationException {
        return clientDetailsRepository.findOneByClientId(clientId);
    }
    /**
     *  添加一个OAuth2ClientDetails对象
     * @param oAuth2ClientDetails
     * @throws ClientAlreadyExistsException
     */
    public void addOAuth2ClientDetails(OAuth2ClientDetails oAuth2ClientDetails) throws ClientAlreadyExistsException {
        if (clientDetailsRepository.findOneByClientId(oAuth2ClientDetails.getClientId()).isPresent()) {
            throw new ClientAlreadyExistsException("Client already exists: " + oAuth2ClientDetails.getClientId());
        }
        if(oAuth2ClientDetails.getClientSecret()==null ) {
            oAuth2ClientDetails.setClientSecret(defaultClientSecretGenerator.generate());
        }
        oAuth2ClientDetails.setClientSecret(passwordEncoder.encode(oAuth2ClientDetails.getClientSecret()));

        clientDetailsRepository.save(oAuth2ClientDetails);
    }

    public void updateOAuth2ClientDetails(OAuth2ClientDetails clientDetails) throws NoSuchClientException {
        OAuth2ClientDetails oAuth2ClientDetails = clientDetailsRepository.findOneByClientId(clientDetails.getClientId())
                .orElseThrow(() -> new NoSuchClientException("No client found with id = " + clientDetails.getClientId()));
        String json = null;
        try {
            if(clientDetails.getAdditionalInformation()!=null) {
                json = mapper.write(clientDetails.getAdditionalInformation());
            }
        }
        catch (Exception e) {
            logger.warn("Could not serialize additional information: " + clientDetails, e);
        }
        oAuth2ClientDetails.setClientName(clientDetails.getClientName() != null ? clientDetails.getClientName():null);
        oAuth2ClientDetails.setResourceIds(clientDetails.getResourceIds() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
                .getResourceIds()) : null);
        oAuth2ClientDetails.setScope(clientDetails.getScope() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
                .getScope()) : null);
        oAuth2ClientDetails.setAuthorizedGrantTypes(clientDetails.getAuthorizedGrantTypes() != null ? StringUtils
                .collectionToCommaDelimitedString(clientDetails.getAuthorizedGrantTypes()) : null);
        oAuth2ClientDetails.setRegisteredRedirectUris(clientDetails.getRegisteredRedirectUri() != null ? StringUtils
                .collectionToCommaDelimitedString(clientDetails.getRegisteredRedirectUri()) : null);
        oAuth2ClientDetails.setAuthorities(clientDetails.getAuthorities() != null ? StringUtils.collectionToCommaDelimitedString(clientDetails
                .getAuthorities()) : null);
        oAuth2ClientDetails.setAccessTokenValiditySeconds(clientDetails.getAccessTokenValiditySeconds()!=null?clientDetails.getAccessTokenValiditySeconds():60 * 60 * 2);
        oAuth2ClientDetails.setRefreshTokenValiditySeconds(clientDetails.getRefreshTokenValiditySeconds() != null ? RefreshTokenValidity.fromSeconds(clientDetails.getRefreshTokenValiditySeconds()):RefreshTokenValidity.SEVEN_DAY);
        oAuth2ClientDetails.setAdditionalInfos(json);
        oAuth2ClientDetails.setAutoApprove(getAutoApproveScopes(clientDetails));

        clientDetailsRepository.save(oAuth2ClientDetails);
    }
    public List<OAuth2ClientDetails> listOAuth2ClientDetails() {
        return clientDetailsRepository.findAll();
    }

    private OAuth2ClientDetails getOauth2ClientDetailsForCreate(ClientDetails clientDetails){

        String json = null;
        try {
            if(clientDetails.getAdditionalInformation()!=null){
                json = mapper.write(clientDetails.getAdditionalInformation());
            }
        } catch (Exception e) {
            logger.warn("Could not serialize additional information: " + clientDetails, e);
        }
        OAuth2ClientDetails oAuth2ClientDetails = OAuth2ClientDetails.builder()
                .clientName(null)
                .clientId(clientDetails.getClientId())
                .clientSecret(clientDetails.getClientSecret())
                .scope(StringUtils.collectionToCommaDelimitedString(clientDetails.getScope()))
                .resourceIds(StringUtils.collectionToCommaDelimitedString(clientDetails.getResourceIds()))
                .authorizedGrantTypes(StringUtils.collectionToCommaDelimitedString(clientDetails.getAuthorizedGrantTypes()))
                .registeredRedirectUris(StringUtils.collectionToCommaDelimitedString(clientDetails.getRegisteredRedirectUri()))
                .authorities(StringUtils.collectionToCommaDelimitedString(AuthorityUtils.authorityListToSet(clientDetails.getAuthorities())))
                .accessTokenValiditySeconds(clientDetails.getAccessTokenValiditySeconds()!=null?clientDetails.getAccessTokenValiditySeconds():60 * 60 * 2)
                .refreshTokenValiditySeconds(clientDetails.getRefreshTokenValiditySeconds() != null ? RefreshTokenValidity.fromSeconds(clientDetails.getRefreshTokenValiditySeconds()):RefreshTokenValidity.SEVEN_DAY)
                .autoApprove(getAutoApproveScopes(clientDetails))
                .additionalInfos(json)
                .build();

        if(oAuth2ClientDetails.getClientSecret()==null ) {
            oAuth2ClientDetails.setClientSecret(defaultClientSecretGenerator.generate());
        }
        oAuth2ClientDetails.setClientSecret(passwordEncoder.encode(clientDetails.getClientSecret()));

        return oAuth2ClientDetails;
    }

    private String getAutoApproveScopes(ClientDetails clientDetails) {
        if (clientDetails.isAutoApprove("true")) {
            return "true"; // all scopes autoapproved
        }
        Set<String> scopes = new HashSet<>();
        for (String scope : clientDetails.getScope()) {
            if (clientDetails.isAutoApprove(scope)) {
                scopes.add(scope);
            }
        }
        return StringUtils.collectionToCommaDelimitedString(scopes);
    }

    private final Function<? super OAuth2ClientDetails, ? extends BaseClientDetails> transformBaseClientDetails = oAuth2ClientDetails -> {
        BaseClientDetails clientDetails = new BaseClientDetails();
        clientDetails.setClientId(oAuth2ClientDetails.getClientId());
        clientDetails.setClientSecret(oAuth2ClientDetails.getClientSecret());
        clientDetails.setAccessTokenValiditySeconds(oAuth2ClientDetails.getAccessTokenValiditySeconds());
        clientDetails.setRefreshTokenValiditySeconds(oAuth2ClientDetails.getRefreshTokenValiditySeconds());
        clientDetails.setAuthorizedGrantTypes(oAuth2ClientDetails.getAuthorizedGrantTypes());
        clientDetails.setScope(oAuth2ClientDetails.getScope());
        clientDetails.setAutoApproveScopes(oAuth2ClientDetails.getScope());
        clientDetails.setResourceIds(oAuth2ClientDetails.getResourceIds());
        clientDetails.setRegisteredRedirectUri(oAuth2ClientDetails.getRegisteredRedirectUri());
        clientDetails.setAdditionalInformation(oAuth2ClientDetails.getAdditionalInformation());
        return clientDetails;
    };

    interface JsonMapper {
        String write(Object input) throws Exception;

        <T> T read(String input, Class<T> type) throws Exception;
    }

    private static JsonMapper createJsonMapper() {
        if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null)) {
            return new JacksonMapper();
        }
        else if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null)) {
            return new Jackson2Mapper();
        }
        return new NotSupportedJsonMapper();
    }

    private static class JacksonMapper implements JsonMapper {
        private org.codehaus.jackson.map.ObjectMapper mapper = new org.codehaus.jackson.map.ObjectMapper();

        @Override
        public String write(Object input) throws Exception {
            return mapper.writeValueAsString(input);
        }

        @Override
        public <T> T read(String input, Class<T> type) throws Exception {
            return mapper.readValue(input, type);
        }
    }

    private static class Jackson2Mapper implements JsonMapper {
        private com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();

        @Override
        public String write(Object input) throws Exception {
            return mapper.writeValueAsString(input);
        }

        @Override
        public <T> T read(String input, Class<T> type) throws Exception {
            return mapper.readValue(input, type);
        }
    }

    private static class NotSupportedJsonMapper implements JsonMapper {
        @Override
        public String write(Object input) throws Exception {
            throw new UnsupportedOperationException(
                    "Neither Jackson 1 nor 2 is available so JSON conversion cannot be done");
        }

        @Override
        public <T> T read(String input, Class<T> type) throws Exception {
            throw new UnsupportedOperationException(
                    "Neither Jackson 1 nor 2 is available so JSON conversion cannot be done");
        }
    }
}
